package org.mozhu.mboot.core.web.controller;

import org.mozhu.mboot.core.data.IBaseService;
import org.mozhu.mboot.core.data.Searchable;
import org.mozhu.mboot.core.data.entity.IEntity;
import org.mozhu.mboot.core.exception.EntityNotFoundException;
import org.mozhu.mboot.core.security.Permissions;
import org.mozhu.mboot.core.support.datatable.DTReturnObject;
import org.mozhu.mboot.core.support.datatable.DTUtils;
import org.mozhu.mboot.core.util.MessageUtils;
import org.mozhu.mboot.core.util.ReflectionUtils;
import org.mozhu.mboot.core.web.AjaxResponse;
import org.mozhu.mboot.core.web.Constants;
import org.mozhu.mboot.core.web.bind.annotation.PageableDefaults;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@SuppressWarnings("unchecked")
public abstract class BaseCRUDController<M extends IEntity<PK>, PK extends Serializable> extends BaseController implements ConfigurableController {

	protected IBaseService<M, PK> baseService;

	protected Permissions permissions = null;

	protected String entityName;

	/**
	 * 实体类型
	 */
	protected final Class<M> entityClass;

	protected BaseCRUDController() {
		this.entityClass = ReflectionUtils.findParameterizedType(getClass(), 0);
		this.entityName = StringUtils.uncapitalize(this.entityClass.getSimpleName());
		setViewPrefix(defaultViewPrefix());
	}

	/**
	 * 权限前缀：如system:user 则生成的新增权限为 system:user:create
	 */
	@Override
	public void setResourceIdentity(String resourceIdentity) {
		if (!StringUtils.isEmpty(resourceIdentity)) {
			permissions = Permissions.newPermissions(resourceIdentity);
		}
	}

	protected M newModel() {
		try {
			return entityClass.newInstance();
		} catch (Exception e) {
			throw new IllegalStateException("can not instantiated model : " + this.entityClass, e);
		}
	}

	/**
	 * 共享的验证规则 验证失败返回true
	 * 
	 * @param m
	 * @param result
	 * @return
	 */
	protected boolean hasError(M m, BindingResult result) {
		Assert.notNull(m, "m should not be null");
		return result.hasErrors();
	}

	@Override
	protected String defaultViewPrefix() {
		String currentViewPrefix = super.defaultViewPrefix();
		if (StringUtils.isEmpty(currentViewPrefix)) {
			currentViewPrefix = this.entityClass.getSimpleName();
		}
		return currentViewPrefix;
	}

	/**
	 * 设置BaseService
	 * 
	 * @param baseService
	 */
	@Autowired
	public void setBaseService(IBaseService<M, PK> baseService) {
		this.baseService = baseService;
	}

	/* 该方法给普通的表格展示使用 */
	@RequestMapping(value = "/list", method = { RequestMethod.GET, RequestMethod.POST })
	@PageableDefaults(sort = "id=desc")
	public String list(HttpServletRequest request, Model model, Searchable searchable) {
		if (permissions != null) {
			this.permissions.assertHasReadPermission();
		}
		model.addAttribute("page", baseService.findAll(searchable));
		setCommonData(model);
		return viewName("list");
	}

	/* 下面两个方法给Ajax DataTable的表格展示使用 */
	@RequestMapping(method = RequestMethod.GET)
	public String list(HttpServletRequest request, Searchable searchable, Model model, @RequestParam(value = "view", required = false, defaultValue = "list") String view) {
		if (permissions != null) {
			this.permissions.assertHasReadPermission();
		}
		setCommonData(model);
		return viewName(view);
	}

	@RequestMapping(headers = "ajax=true", method = RequestMethod.POST)
	@ResponseBody
	@PageableDefaults(sort = "id=desc")
	public Object list(HttpServletRequest request, Searchable searchable) {
		if (permissions != null) {
			this.permissions.assertHasReadPermission();
		}
		Page<M> page = baseService.findAll(searchable);
		DTReturnObject<M> dtReturnObject = DTUtils.toDTReturnObject(page);
		dtReturnObject.setDraw(request.getParameter("draw"));
		dtReturnObject.setExtraData(getExtraData(request, page));
		return needConvert() ? convert(dtReturnObject) : dtReturnObject;
	}

	protected Map<String, Object> getExtraData(HttpServletRequest request, Page<M> page) {
		return null;
	}

	protected boolean needConvert() {
		return false;
	}

	protected <T> DTReturnObject<T> convert(DTReturnObject<M> dtReturnObject) {
		DTReturnObject<T> dtReturnObjectReturn = new DTReturnObject<T>();
		dtReturnObjectReturn.setDraw(dtReturnObject.getDraw());
		dtReturnObjectReturn.setError(dtReturnObject.getError());
		dtReturnObjectReturn.setRecordsFiltered(dtReturnObject.getRecordsFiltered());
		dtReturnObjectReturn.setRecordsTotal(dtReturnObject.getRecordsTotal());
		dtReturnObjectReturn.setData(convert(dtReturnObject.getData()));
		dtReturnObjectReturn.setExtraData(dtReturnObject.getExtraData());
		return dtReturnObjectReturn;
	}

	protected <T> List<T> convert(List<M> data) {
		return (List<T>) data;
	}

	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	public String view(Model model, @PathVariable("id") PK id) {
		if (permissions != null) {
			this.permissions.assertHasReadPermission();
		}
		model.addAttribute(Constants.OP_NAME, OPERATION_VIEW);
		setCommonData(model);
		M m = baseService.findById(id);
		assertNotNull(m, id);
		model.addAttribute("m", m);
		setExtraData(m, model);
		return viewName("editForm");
	}

	protected void assertNotNull(M m, PK id) {
		if (m == null) {
			throw new EntityNotFoundException(entityClass.getSimpleName(), id);
		}
	}

	@RequestMapping(value = "create", method = RequestMethod.GET)
	public String showCreateForm(HttpServletRequest request, Model model) {
		if (permissions != null) {
			this.permissions.assertHasCreatePermission();
		}

		model.addAttribute(Constants.OP_NAME, OPERATION_ADD);
		setCommonData(model);
		if (!model.containsAttribute("m")) {
			model.addAttribute("m", newModel());
		}
		return viewName("editForm");
	}

	@RequestMapping(value = "create", method = RequestMethod.POST)
	public String create(HttpServletRequest request, Model model, @Valid @ModelAttribute("m") M m, BindingResult result, @RequestParam(value = Constants.BACK_URL, required = false) String backURL,
                         RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasCreatePermission();
		}
		if (hasError(m, result)) {
			return showCreateForm(request, model);
		}
		try {
			beforeCreate(request, model, m, result, backURL, redirectAttributes);
			baseService.save(m);
			afterCreate(request, model, m, result, backURL, redirectAttributes);
			redirectAttributes.addFlashAttribute(Constants.MESSAGE, CREATE_SUCCESS);
		} catch (RuntimeException e) {
			logger.error("create fail", e);
			redirectAttributes.addFlashAttribute(Constants.ERROR, CREATE_FAIL);
		}
		return redirect(backURL);
	}

	protected void afterCreate(HttpServletRequest request, Model model, M m, BindingResult result, String backURL, RedirectAttributes redirectAttributes) {
		// let someone to override
	}

	protected void beforeCreate(HttpServletRequest request, Model model, M m, BindingResult result, String backURL, RedirectAttributes redirectAttributes) {
		// let someone to override
	}

	// -- ajax create -- //
	@RequestMapping(headers = "ajax=true", value = "create", method = RequestMethod.POST)
	@ResponseBody
	public Object create(HttpServletRequest request, Model model, @Valid @ModelAttribute("m") M m, BindingResult result, RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasCreatePermission();
		}

		if (hasError(m, result)) {
			// TODO to return more fail detail
			return AjaxResponse.fail(MessageUtils.message(CREATE_FAIL));
		}
		try {
			beforeCreate(request, model, m, result, null, redirectAttributes);
			baseService.save(m);
			afterCreate(request, model, m, result, null, redirectAttributes);
			return AjaxResponse.success(MessageUtils.message(CREATE_SUCCESS), m);
		} catch (RuntimeException e) {
			logger.error("create fail", e);
			return AjaxResponse.fail(MessageUtils.message(CREATE_FAIL));
		}
	}

	@RequestMapping(value = "{id}/update", method = RequestMethod.GET)
	public String showUpdateForm(@PathVariable("id") PK id, Model model) {
		if (permissions != null) {
			this.permissions.assertHasUpdatePermission();
		}
		model.addAttribute(Constants.OP_NAME, OPERATION_EDIT);
		setCommonData(model);
		M m = baseService.findById(id);
		assertNotNull(m, id);
		model.addAttribute("m", m);
		setExtraData(m, model);
		return viewName("editForm");
	}

	protected void setExtraData(M m, Model model) {
	}

	@RequestMapping(value = "{id}/update", method = RequestMethod.POST)
	public String update(HttpServletRequest request, Model model, @Valid @ModelAttribute("m") M m, BindingResult result, @RequestParam(value = Constants.BACK_URL, required = false) String backURL,
                         RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasUpdatePermission();
		}
		if (hasError(m, result)) {
			return showUpdateForm(m.getId(), model);
		}
		try {
			beforeUpdate(request, model, m, result, backURL, redirectAttributes);
			baseService.save(m);
			afterUpdate(request, model, m, result, backURL, redirectAttributes);
			redirectAttributes.addFlashAttribute(Constants.MESSAGE, UPDATE_SUCCESS);
		} catch (RuntimeException e) {
			logger.error("update fail", e);
			redirectAttributes.addFlashAttribute(Constants.ERROR, UPDATE_FAIL);
		}
		return redirect(backURL);
	}

	protected void afterUpdate(HttpServletRequest request, Model model, M m, BindingResult result, String backURL, RedirectAttributes redirectAttributes) {
		// let someone to override
	}

	protected void beforeUpdate(HttpServletRequest request, Model model, M m, BindingResult result, String backURL, RedirectAttributes redirectAttributes) {
		// let someone to override
	}

	// -- ajax update -- //
	@RequestMapping(headers = "ajax=true", value = "{id}/update", method = RequestMethod.POST)
	@ResponseBody
	public Object update(HttpServletRequest request, Model model, @Valid @ModelAttribute("m") M m, BindingResult result, RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasUpdatePermission();
		}
		if (hasError(m, result)) {
			// TODO to return more fail detail
			return AjaxResponse.fail(MessageUtils.message(UPDATE_FAIL));
		}
		try {
			beforeUpdate(request, model, m, result, null, redirectAttributes);
			baseService.save(m);
			afterUpdate(request, model, m, result, null, redirectAttributes);
			return AjaxResponse.success(MessageUtils.message(UPDATE_SUCCESS), m);
		} catch (RuntimeException e) {
			logger.error("create fail", e);
			return AjaxResponse.fail(MessageUtils.message(UPDATE_FAIL));
		}
	}

	@RequestMapping(value = "{id}/delete", method = { RequestMethod.GET, RequestMethod.POST })
	public String delete(HttpServletRequest request, @PathVariable("id") PK id, @RequestParam(value = Constants.BACK_URL, required = false) String backURL, RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasDeletePermission();
		}
		try {
			M m = baseService.findById(id);
			beforeDelete(request, m);
			baseService.delete(m);
			afterDelete(request, m);
			redirectAttributes.addFlashAttribute(Constants.MESSAGE, DELETE_SUCCESS);
		} catch (RuntimeException e) {
			logger.error("delete fail", e);
			redirectAttributes.addFlashAttribute(Constants.ERROR, DELETE_FAIL);
		}
		return redirect(backURL);
	}

	// -- ajax delete -- //
	@RequestMapping(headers = "ajax=true", value = "{id}/delete", method = { RequestMethod.GET, RequestMethod.POST })
	@ResponseBody
	public Object delete(HttpServletRequest request, @PathVariable("id") PK id) {
		if (permissions != null) {
			this.permissions.assertHasDeletePermission();
		}
		try {
			M m = baseService.findById(id);
			beforeDelete(request, m);
			baseService.delete(m);
			afterDelete(request, m);
			return AjaxResponse.success(MessageUtils.message(DELETE_SUCCESS));
		} catch (RuntimeException e) {
			logger.error("delete fail", e);
			return AjaxResponse.fail(MessageUtils.message(DELETE_FAIL));
		}
	}

	protected void afterDelete(HttpServletRequest request, M m) {
		// let someone to implement
	}

	protected void beforeDelete(HttpServletRequest request, M m) {
		// let someone to implement
	}

	@RequestMapping(value = "batch/delete", method = { RequestMethod.GET, RequestMethod.POST })
	public String deleteInBatch(HttpServletRequest request, @RequestParam(value = "ids", required = false) PK[] ids, @RequestParam(value = Constants.BACK_URL, required = false) String backURL,
                                RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasDeletePermission();
		}
		try {
			List<M> all = baseService.findAllById(Arrays.asList(ids));
			for (M m : all) {
				beforeDelete(request, m);
			}
			baseService.deleteAll(all);
			for (M m : all) {
				afterDelete(request, m);
			}
			redirectAttributes.addFlashAttribute(Constants.MESSAGE, DELETE_SUCCESS);
		} catch (RuntimeException e) {
			logger.error("batch delete fail", e);
			redirectAttributes.addFlashAttribute(Constants.ERROR, DELETE_FAIL);
		}
		return redirect(backURL);
	}

	// -- ajax batchDelete -- //
	@RequestMapping(headers = "ajax=true", value = "batch/delete", method = { RequestMethod.GET, RequestMethod.POST })
	@ResponseBody
	public Object deleteInBatch(HttpServletRequest request, @RequestParam(value = "ids", required = false) PK[] ids, RedirectAttributes redirectAttributes) {
		if (permissions != null) {
			this.permissions.assertHasDeletePermission();
		}
		try {
			List<M> all = baseService.findAllById(Arrays.asList(ids));
			for (M m : all) {
				beforeDelete(request, m);
			}
			baseService.deleteAll(all);
			for (M m : all) {
				afterDelete(request, m);
			}
			return AjaxResponse.success(MessageUtils.message(DELETE_SUCCESS));
		} catch (RuntimeException e) {
			logger.error("batch delete fail", e);
			return AjaxResponse.fail(MessageUtils.message(DELETE_FAIL));
		}
	}

	/**
	 * 所有RequestMapping方法调用前的Model准备方法, 实现Struts2
	 * Preparable二次部分绑定的效果,先根据form的id从数据库查出User对象,再把Form提交的内容绑定到该对象上。
	 * 因为仅update()方法的form中有id属性，因此仅在update时实际执行.
	 */
	@ModelAttribute
	public void getModel(@RequestParam(value = "id", required = false) PK id, Model model) {
		if (id != null) {
			model.addAttribute("m", baseService.findById(id));
		}
	}

	@Override
	protected void setCommonData(Model model) {
		super.setCommonData(model);
		if (permissions != null) {
			model.addAttribute("resourceIdentity", permissions.getResourceIdentity());
		}
		// 供页面显示
		model.addAttribute("entityName", entityName);
	}

	public void setEntityName(String entityName) {
		this.entityName = entityName;
	}

}
